package suncertify.db;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;

import suncertify.application.Log;
import suncertify.application.Strings;
import suncertify.application.exceptions.NullEmptyParameterException;

/**
 * <p>
 * This class provides read and write access to the applications database file. The database file has the following
 * schema:
 * 
 * <p>
 * <b>Start of file.</b>
 * <ol>
 * <li><b>Bytes 0 to 3 (4 byte numeric)</b> Magic cookie value identifies this as a data file.</li>
 * <li><b>Bytes 4 to 7 (4 byte numeric)</b> Offset to start of record zero.</li>
 * <li><b>Bytes 8 to 9 (2 byte numeric)</b> Number of fields in each record.</li>
 * </ol>
 * 
 * <p>
 * <b>Schema description section</b>. (defines each field name length, name and field length).
 * <ul>
 * <li>Repeated for each field in a record:</li>
 * <li>2 byte numeric, length in bytes of field name.</li>
 * <li>n bytes of field name (defined by previous entry).</li>
 * <li>2 byte numeric, field length in bytes.</li>
 * <li>End of repeating block</li>
 * </ul>
 * 
 * 
 * <p>
 * <b>Data section (offset into file equal to "offset to start of record zero" value)</b>
 * <ul>
 * <li>Repeat to end of file:</li>
 * <li>
 * 2 byte flag. 00 implies valid record, 0x8000 implies deleted record</li>
 * <li>
 * Record containing fields in order specified in schema section, no separators between fields, each field fixed length
 * at maximum specified in schema information</li>
 * <li>
 * End of file</li>
 * </ul>
 * 
 * All numeric values are stored in the header information use the formats of the DataInputStream and DataOutputStream
 * classes. All text values, and all fields (which are text only), contain only 8 bit characters, null terminated if
 * less than the maximum length for the field. The character encoding is 8 bit US ASCII.
 * 
 * <p>
 * <b>The database that Bodgitt and Scarper uses contains the following fields:</b>
 * <p>
 * <table>
 * <tr>
 * <td><b>Field Descriptive Name</b></td>
 * <td><b>Database Field Name</b></td>
 * <td><b>Field Length</b></td>
 * <td><b>Detailed Description</b></td>
 * </tr>
 * 
 * <tr>
 * <td>Subcontractor Name</td>
 * <td>name</td>
 * <td>32</td>
 * <td>The name of the subcontractor this record relates to.</td>
 * </tr>
 * 
 * <tr>
 * <td>City</td>
 * <td>location</td>
 * <td>32</td>
 * <td>The locality in which this contractor works</td>
 * </tr>
 * 
 * <tr>
 * <td>Types of work performed</td>
 * <td>specialties</td>
 * <td>64</td>
 * <td>Comma separated list of types of work this contractor can perform.</td>
 * </tr>
 * 
 * <tr>
 * <td>Number of staff in organization</td>
 * <td>size</td>
 * <td>6</td>
 * <td>The number of workers available when this record is booked</td>
 * </tr>
 * 
 * <tr>
 * <td>Hourly charge</td>
 * <td>rate</td>
 * <td>8</td>
 * <td>Charge per hour for the subcontractor. This field includes the currency symbol</td>
 * </tr>
 * 
 * <tr>
 * <td>Customer holding this record</td>
 * <td>owner</td>
 * <td>8</td>
 * <td>The id value (an 8 digit number) of the customer who has booked this. Note that for this application, you should
 * assume that customers and CSRs know their customer ids. The system you are writing does not interact with these
 * numbers, rather it simply records them. If this field is all blanks, the record is available for sale.</td>
 * </tr>
 * 
 * </table>
 * 
 * 
 * 
 * <p>
 * This class is constructed with the path of the database file, when the class is constructed it performs the following
 * actions:
 * <ul>
 * <li>Creates one instance of the {@link RandomAccessFile} class, all IO is done using this instance.</li>
 * <li>Reads the database file schema section and stores the schema information in member variables. If the file cannot
 * be accessed or the internal schema of the file is not correct then exceptions are raised as declared in the
 * constructor.</li>
 * <li>If the internal schema of the file is correct, the class creates one instance of the private
 * <code>RecordsCache</code> class. All records from the database are read and saved in the internal cache class.</li>
 * </ul>
 * 
 * <p>
 * When the constructor finishes successfully the class contains all the database records in its internal cache. The
 * <code>FileAccess</code> class should be created only once in the application, it is the responsibility of the caller
 * to guarantee this.
 * 
 * <p>
 * All record read operations and search operations retrieve data from the internal cache, no disk access is required
 * for read and search operations. Write operations like update/delete/add records access the file system first then
 * update the cache accordingly.
 * 
 * <p>
 * No function in the class is thread safe! the class does not use any kind of synchronization.
 * 
 */
class FileAccess {

    /**
     * Constant that defines the US ASCII character set.
     */
    private final String US_ASCII = "US-ASCII";

    /**
     * Constant that defines the read and write file access mode.
     */
    private final String FILE_ACCESS_MODE = "rw";

    /**
     * Constant that defines the length of the record flag.
     */
    private final int RECORD_FLAG_LENGTH = 2;

    /**
     * Constant that defines the database file content ID. The application only considers files with this content ID as
     * database files.
     */
    private final int FILE_CONTENTID = 0x00000202;

    /**
     * Constant that defines the offset of the record schema in the database file.
     */
    private final long RECORD_SCHEMA_OFFSET = 10;

    /**
     * Defines the offset to the first record in the database file.
     */
    private int offsetToFirstRecord;

    /**
     * Defines the number of fields in the record.
     */
    private int recordFieldCount;

    /**
     * Defines the total number of records in the database.
     */
    private long recordCount;

    /**
     * Defines the record length in bytes.
     */
    private int recordLength;

    /**
     * Defines the length of the longest record field.
     */
    private int maxFieldLength = -1;

    /**
     * Declares an array of field names as defined in the schema section.
     */
    private String[] fieldNames;

    /**
     * Declares an array of field lengths. Each element represents the field length.
     */
    private int[] fieldLengths;

    /**
     * Declares an array of field offsets. Each element represents the offset of the field from the record start.
     */
    private int[] fieldOffsets;

    /**
     * Declares a {@link StringBuilder} used to pad short fields that with space characters.
     */
    private StringBuilder spacesString = new StringBuilder();

    /**
     * Declares the {@link RecordsCache} class variable.
     */
    private RecordsCache cache;

    /**
     * Declares the file access object..
     */
    private RandomAccessFile randomAccessFile = null;

    /**
     * Constructs the class using the database file path. The constructor does the following:
     * <ul>
     * <li>Creates one instance of the {@link RandomAccessFile}. This class handles all the IO operations.</li>
     * <li>Reads the file schema information described in the class header and saves it into class member variables.</li>
     * <li>Validates the schema of the file by calling the {@link #isFileSchemaValid()}.</li>
     * <li>Creates one instance of the <code>RecordsCache</code> class and fills it with all records from the database.</li>
     * </ul>
     * 
     * @param filePath
     *            Database file path.
     * @throws NullEmptyParameterException
     *             Thrown when the <code>filePath</code> is null or empty string.
     * @throws DatabaseFormatException
     *             Thrown if the file does not represent the applications database file or when the internal schema of
     *             the file is corrupt. The exception can be raised by one of these functions which are called in this
     *             order: {@link #readFileSchemaInfo()}, {@link #readRecordSchemaInfo()} or {@link #isFileSchemaValid()}
     * 
     * @throws DatabaseAccessException
     *             Thrown if the database file does not exist or an {@link java.io.IOException} is thrown while
     *             accessing the file.
     */
    FileAccess(String filePath) throws DatabaseAccessException, DatabaseFormatException {

	if (filePath == null || filePath.trim().isEmpty()) {
	    throw new NullEmptyParameterException("filePath");
	}

	try {
	    this.randomAccessFile = new RandomAccessFile(filePath, FILE_ACCESS_MODE);
	} catch (FileNotFoundException e) {
	    throw new DatabaseAccessException(Strings.MSG_DATABASE_FILE_NOT_FOUND, e);
	}

	try {
	    this.readFileSchemaInfo();
	    this.readRecordSchemaInfo();
	    this.isFileSchemaValid();

	    this.cache = new RecordsCache();
	    this.fillCache();

	    Log.logInfo("FileAccess constructor finished successfully.");
	} catch (IOException e) {
	    Log.logInfo("IOException inf FileAccess constructor.");
	    throw new DatabaseAccessException(Strings.MSG_DATABASE_READ_ERROR, e);
	}
    }

    /**
     * Determines if the record is deleted in the records cache.
     * 
     * @param recordNumber
     *            The record number.
     * @return True if the record is deleted, returns false otherwise.
     */
    boolean isRecordDeleted(long recordNumber) {

	return this.cache.isRecordDeleted(recordNumber);
    }

    /**
     * Returns the record count of the database file.
     * 
     * @return The record count.
     */
    long getRecordCount() {

	return this.recordCount;
    }

    /**
     * Returns the number of fields in the database record.
     * 
     * @return Number of fields in the database record.
     */
    int getRecordFieldCount() {

	return this.recordFieldCount;
    }

    /**
     * Returns the record field count as <code>String</code>
     * 
     * @return The record field count as <code>String</code>
     */
    String getRecordFieldCountAsString() {

	return Integer.toString(this.recordFieldCount);
    }

    /**
     * Determines if the provided parameter represents a valid record number. A valid record number must be
     * positive/zero and less than the total number of records. Example, if record count = 100 then valid record numbers
     * are 0 to 99.
     * 
     * <p>
     * Note, the record number can refer to a deleted or not deleted record.
     * 
     * @param recordNumber
     *            The record number.
     * @return True if the record number is valid, returns false otherwise.
     */
    boolean isRecordNumberValid(long recordNumber) {

	if (recordNumber < 0 || recordNumber >= this.recordCount) {

	    return false;
	} else {

	    // Data.log("FileAccess.isRecordNumberValid", "function end");
	    return true;
	}
    }

    /**
     * Determines if the length of provided array equals the number of fields in the record.
     * 
     * @param data
     *            An array of <code>String</code> values that represent a database record.
     * @return True if the number of elements in the array equals the number of fields in the record, returns false
     *         otherwise.
     */
    boolean isDataFieldLengthValid(String[] data) {

	if (data.length != this.recordFieldCount) {
	    return false;
	}

	return true;
    }

    /**
     * Simply calls {@link RandomAccessFile#close()} on the internal instance of {@link RandomAccessFile}.
     * 
     * @throws IOException
     *             See {@link RandomAccessFile#close()}
     */
    void close() throws IOException {

	this.randomAccessFile.close();
    }

    /**
     * Creates a new record in the database file and the records cache. The new record is created either:
     * <ol>
     * <li>By reusing the files space of a previously deleted record. The record flag is changed from deleted to
     * undeleted is explained in the file schema. The record count is not increased. If multiple records are already
     * deleted in the database then the record with the smallest record number is reused for the new record.</li>
     * <li>If no deleted records exist then the new record is appended to the database file end, the record count is
     * incremented by one.</li>
     * </ol>
     * 
     * @param data
     *            The records data. Each element in the array corresponds to one record field.
     * @return The number of the created record. This value depends on the location in file were the record is created
     *         as explained in the function header.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    long createNewRecord(String[] data) throws IOException {

	// determine were to create the new record
	long emptyRecordNumber = this.cache.getFirstDeletedRecord();

	// a minus value means there are no deleted records to reuse, in this case the record is appended to the end of
	// the file.
	if (emptyRecordNumber < 0) {

	    // write record to the end of the file
	    this.writeRecordToFile(this.recordCount, data);

	    // create the record in the cache at the same location.
	    this.cache.addRecord(this.recordCount, false, data);

	    // increase record count
	    ++this.recordCount;

	    // return index of the last newly added record.
	    return this.recordCount - 1;

	} else {

	    // undelete the previously deleted record and save the records data.
	    this.undeleteFileRecord(emptyRecordNumber);
	    this.writeRecordToFile(emptyRecordNumber, data);

	    // undelete and write the record into the cache in the same lcoation.
	    this.cache.undeleteRecord(emptyRecordNumber);
	    this.cache.writeRecord(emptyRecordNumber, data);

	    return emptyRecordNumber;
	}
    }

    /**
     * Writes the provided record data in the database file and the records cache at the location referred to by
     * <code>recordNumber</code>. The function does not validate the input parameters. The function calls the
     * {@link #writeRecordToFile(long, String[])} and {@link RecordsCache#writeRecord(long, String[])}.
     * 
     * @param recordNumber
     *            The number of the record to write to.
     * @param data
     *            The records data, each element in this array corresponds to one record field.
     * @throws IOException
     *             Thrown if any I/O error occurs while accessing the database file.
     */
    void writeRecord(long recordNumber, String[] data) throws IOException {

	this.writeRecordToFile(recordNumber, data);

	this.cache.writeRecord(recordNumber, data);
    }

    /**
     * Reads the record referred to by <code>recordNumber</code> from the records cache. The call is simply delegated to
     * {@link RecordsCache#readRecord(long)}.
     * 
     * @param recordNumber
     *            The record number.
     * @return The records data as a <code>String</code> array. Each element in the array corresponds to one record
     *         field.
     */
    String[] readRecordFromCache(long recordNumber) {

	return this.cache.readRecord(recordNumber);
    }

    /**
     * Deletes the record referred to by <code>recordNumber</code> from the database file and from the records cache.
     * The size of the database file is not changed after the deletion, only the record tag is changed from undeleted to
     * deleted as described in the database file schema in the class header. The function does not validate the input
     * parameter, it simply calls {@link #deleteRecordFromFile(long)} and {@link RecordsCache#deleteRecord(long)}.
     * 
     * @param recordNumber
     *            The number of the record to be deleted.
     * @throws IOException
     *             Thrown if any I/O error occurs during file access.
     */
    void deleteRecord(long recordNumber) throws IOException {

	this.deleteRecordFromFile(recordNumber);

	this.cache.deleteRecord(recordNumber);
    }

    /**
     * Simply passes the parameters and calls: {@link RecordsCache#isNameLocationUnique(String, String, long)}
     * 
     * @param name
     *            See {@link RecordsCache#isNameLocationUnique(String, String, long)}
     * @param location
     *            See {@link RecordsCache#isNameLocationUnique(String, String, long)}
     * @param skipRecordNumber
     *            See {@link RecordsCache#isNameLocationUnique(String, String, long)}
     * @return See {@link RecordsCache#isNameLocationUnique(String, String, long)}
     */
    boolean isNameLocationUnique(String name, String location, long skipRecordNumber) {

	return this.cache.isNameLocationUnique(name, location, skipRecordNumber);
    }

    /**
     * Determines if the specified record is already booked to a customer. This function simply calls
     * {@link RecordsCache#isRecordBooked(long)}
     * 
     * @param recordNo
     *            The record number of the contractor to be checked.
     * @return True if the record is booked, false otherwise.
     */
    boolean isRecordBooked(long recordNo) {

	return this.cache.isRecordBooked(recordNo);
    }

    /**
     * Reads all records (deleted and undeleted) from the database file and caches them in the {@link RecordsCache}
     * class member.
     * 
     * @throws IOException
     *             Thrown if any I/O occurs.
     */
    private void fillCache() throws IOException {

	for (long i = 0; i < this.recordCount; ++i) {

	    this.cache.addRecord(i, this.isFileRecordDeleted(i), this.readRecordFromFile(i));
	}

	Log.logInfo("FileAccess successfully filled the records cache.");
    }

    /**
     * Reads the following file schema information: (Refer to the file schema description documentation in the class
     * header.)
     * <ol>
     * <li>Byte 0 to 3 which represent the file content ID.</li>
     * <li>Byte 4 to 7 which represent the offset of record 0 in the database file.</li>
     * <li>Byte 8 to 9 which represents the number of fields in the record.</li>
     * </ol>
     * 
     * <p>
     * This class uses the internal reference of the {@link RandomAccessFile}. The function starts reading from file
     * position 0. This function should be called only once when the {@link FileAccess} class is constructed.
     * 
     * @throws IOException
     *             If any I/O error occurs.
     * @throws DatabaseFormatException
     *             <ol>
     *             <li>If bytes 0-3 are not equal to {@link #FILE_CONTENTID}.</li><li>If the offset to record 0 is a
     *             negative integer.</li><li>If the record field count is a negative integer.</li>
     *             </ol>
     */
    private void readFileSchemaInfo() throws IOException, DatabaseFormatException {

	this.randomAccessFile.seek(0);

	if (this.randomAccessFile.readInt() != FILE_CONTENTID) {
	    this.randomAccessFile.close();

	    Log.logSevere(Strings.MSG_DATABASE_WRONG_FORMAT_ERROR);
	    throw new DatabaseFormatException(Strings.MSG_DATABASE_WRONG_FORMAT_ERROR);
	}

	// read the offset to the first record
	this.offsetToFirstRecord = this.randomAccessFile.readInt();

	if (this.offsetToFirstRecord < 0) {
	    Log.logSevere("Database schema error! offset to first record is negative!");
	    throw new DatabaseFormatException(Strings.MSG_DATABASE_WRONG_FORMAT_ERROR);
	}

	// read number of fields pro record
	this.recordFieldCount = this.randomAccessFile.readUnsignedShort();

	if (this.recordFieldCount < 0) {
	    Log.logSevere("Database schema error! record count is negative!");
	    throw new DatabaseFormatException(Strings.MSG_DATABASE_WRONG_FORMAT_ERROR);
	}

	this.fieldNames = new String[this.recordFieldCount];
	this.fieldLengths = new int[this.recordFieldCount];
	this.fieldOffsets = new int[this.recordFieldCount];
    }

    /**
     * Reads the record schema information into member variables. Please refer to the file record schema section as
     * class documentation.
     * <ul>
     * <li>Reads the record field names and lengths into member variables.</li>
     * <li>Calculates the records length in bytes and stores it in a member variable.</li>
     * <li>Calculates the offset of each field in the record and stores it in a member array variable.</li>
     * <li>Determines the maximum field length and stores it in a member variable.</li>
     * <li>Calculates the number of records in the file as:
     * <code>recordCount = (FileLengthInBytes - OffsetToFirstRecord)/(RecordLengthInBytes + 2)</code> where two is the
     * size in bytes of the record flag.</li>
     * </ul>
     * 
     * <p>
     * This class uses the internal reference of the {@link RandomAccessFile}. The function starts from file position
     * 10. This function should be called only once when the {@link FileAccess} class is constructed. This function must
     * be called after {@link #readFileSchemaInfo()}
     * 
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private void readRecordSchemaInfo() throws IOException {

	int fieldLength = 0;
	byte[] buffer = null;

	this.randomAccessFile.seek(RECORD_SCHEMA_OFFSET);

	for (int i = 0; i < this.recordFieldCount; i++) {

	    // get field name length
	    fieldLength = this.randomAccessFile.readUnsignedShort();
	    buffer = new byte[fieldLength];

	    this.randomAccessFile.readFully(buffer);
	    this.fieldNames[i] = new String(buffer, US_ASCII);

	    // get field length
	    this.fieldLengths[i] = this.randomAccessFile.readUnsignedShort();

	    // calculate record length
	    this.recordLength += this.fieldLengths[i];

	    // calculate field offsets
	    this.fieldOffsets[i] = this.recordLength - this.fieldLengths[i];

	    // find the longest field
	    if (this.maxFieldLength < 0) {

		this.maxFieldLength = this.fieldLengths[i];
	    } else {

		if (this.fieldLengths[i] > this.maxFieldLength) {
		    this.maxFieldLength = this.fieldLengths[i];
		}
	    }
	}

	// calculate recordCount
	this.recordCount = (this.randomAccessFile.length() - (long) this.offsetToFirstRecord)
		/ (this.recordLength + RECORD_FLAG_LENGTH);

	// initialize the empty record byte array
	for (int i = 0; i < this.maxFieldLength; i++) {
	    spacesString.append(" ");
	}
    }

    /**
     * Validates the correctness of the file and record schema information read by {@link #readFileSchemaInfo()} and
     * {@link #readRecordSchemaInfo()}. If the schema information is correct the function simply returns, otherwise it
     * raises {@link DatabaseFormatException}
     * 
     * @throws DatabaseFormatException
     *             Thrown if:
     *             <ol>
     *             <li>(FileLength - OffsetToFirstRecord) < 0</li>
     *             <li>(FileLength - OffsetToFirstRecord) is not an integer multiple of: RecordSize + 2</li>
     *             </ol>
     * 
     * @throws IOException
     *             If an I/O error occurs.
     */
    private void isFileSchemaValid() throws DatabaseFormatException, IOException {

	long dataLength = this.randomAccessFile.length() - (long) this.offsetToFirstRecord;

	if (dataLength <= 0) {
	    Log.logSevere(Strings.MSG_DATABASE_OFFSET_ERROR);
	    throw new DatabaseFormatException(Strings.MSG_DATABASE_OFFSET_ERROR);
	}

	if (dataLength % (long) (this.recordLength + RECORD_FLAG_LENGTH) != 0) {
	    Log.logSevere(Strings.MSG_DATABASE_LENGTH_ERROR);
	    throw new DatabaseFormatException(Strings.MSG_DATABASE_LENGTH_ERROR);
	}

	/*
	 * If you are here then we consider the database to be valid. For the scope of this assignment no further schema
	 * and data validations are done.
	 */
    }

    /**
     * Calculates the position of the <code>recNo</code> in the database file.
     * 
     * @param recNo
     *            The record number.
     * @return The position of the record in the database file.
     */
    private long getRecordPosition(long recNo) {

	return recNo * ((long) this.recordLength + RECORD_FLAG_LENGTH) + (long) this.offsetToFirstRecord;
    }

    /**
     * Moves the index/position of the {@link RandomAccessFile} object to the position specified by <code>recNo</code>.
     * 
     * @param recNo
     *            The number of the record.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private void moveIndexToRecord(long recNo) throws IOException {

	long recordOffset = this.getRecordPosition(recNo);

	this.randomAccessFile.seek(recordOffset);
    }

    /**
     * Moves the index/position of the internal {@link RandomAccessFile} object to the position specified by the
     * <code>recNo</code> and <code>fieldID</code>.
     * 
     * @param recNo
     *            The records number.
     * @param fieldID
     *            The index/ID of the record's field.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private void moveIndexToField(long recNo, int fieldID) throws IOException {

	long recordOffset = this.getRecordPosition(recNo) + RECORD_FLAG_LENGTH + (long) this.fieldOffsets[fieldID];

	this.randomAccessFile.seek(recordOffset);
    }

    /**
     * Changes the flag of the <code>recNo</code> to not deleted in the database file. This function is usually used
     * when the space of a deleted record in the database file is reused for a new record.
     * 
     * @param recNo
     *            The number of the record.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private void undeleteFileRecord(long recNo) throws IOException {

	this.moveIndexToRecord(recNo);

	byte[] validRecordID = new byte[] { (byte) 0x00, 0x00 };

	this.randomAccessFile.write(validRecordID);
    }

    /**
     * Determines if the <code>recNo</code> is deleted in the database file.
     * 
     * @param recNo
     *            The record number.
     * @return True if the record is deleted in the database file, false otherwise.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private boolean isFileRecordDeleted(long recNo) throws IOException {

	this.moveIndexToRecord(recNo);

	return this.randomAccessFile.readUnsignedShort() == 0x8000;
    }

    /**
     * Deletes the record referred to by <code>recNo</code> from the database file. This function only changes the flag
     * of the record in the database. The size of the database file is not changed. The space of the deleted record is
     * reused by create record operations.
     * 
     * @param recNo
     *            The record number to be deleted.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private void deleteRecordFromFile(long recNo) throws IOException {

	this.moveIndexToRecord(recNo);

	byte[] deletedID = new byte[] { (byte) 0x80, 0x00 };

	this.randomAccessFile.write(deletedID);

	Log.logInfo("FileAccess.deleteRecordFromFile() successsfully deleted record " + Long.toString(recNo));
    }

    /**
     * Writes the provided record data into the database file in the position referred to by <code>recNo</code>. The
     * record flag is set to 0x0000. The input parameters are not validated.
     * 
     * <p>
     * Each element in the <code>recordData</code> parameter represents a record field. The record fields are written to
     * the file as follows:
     * <ul>
     * <li>Record fields longer than the allowed size are trimmed.</li>
     * <li>Record fields smaller than the allowed size are padded with space characters.</li>
     * <li>Null record fields are written as space characters.</li>
     * </ul>
     * 
     * @param recNo
     *            The record number to be written to.
     * @param recordData
     *            The records data as <code>String</code> array. Each array element represents a record field.
     * @throws IOException
     *             Throw if any I/O errors occur.
     */
    private void writeRecordToFile(long recNo, String[] recordData) throws IOException {

	StringBuilder stringBuilder = new StringBuilder();
	stringBuilder.append(new String(new byte[] { 0x00, 0x00 }, US_ASCII));
	String recordDataField = null;
	String missingSpaces = null;

	for (int i = 0; i < this.recordFieldCount; i++) {

	    if (recordData[i] == null) {
		recordDataField = spacesString.substring(0, this.fieldLengths[i]);

	    } else if (recordData[i].length() > this.fieldLengths[i]) {

		recordDataField = recordData[i].substring(0, this.fieldLengths[i]);

	    } else if (recordData[i].length() < this.fieldLengths[i]) {

		missingSpaces = spacesString.substring(0, this.fieldLengths[i] - recordData[i].length());
		recordDataField = recordData[i].concat(missingSpaces);

	    } else {

		recordDataField = recordData[i];
	    }

	    stringBuilder.append(recordDataField);
	}

	this.moveIndexToRecord(recNo);
	this.randomAccessFile.write(stringBuilder.toString().getBytes(US_ASCII));

	Log.logInfo("FileAccess.writeRecordToFile() successfully wrote record " + Long.toString(recNo));
    }

    /**
     * Reads the <code>recNo</code> record fields from the database file. The input parameter is not validated by the
     * function.
     * 
     * @param recNo
     *            The record number whose fields are to be read.
     * @return The record fields as a string array where each array element represents a record field.
     * @throws IOException
     *             Thrown if any I/O error occurs.
     */
    private String[] readRecordFromFile(long recNo) throws IOException {

	// move to the record position in the database file.
	this.moveIndexToField(recNo, 0);

	// declare and initialize the byte and string arrays use to hold the records data.
	byte[] recordBytes = new byte[this.recordLength];
	String[] recordFields = new String[this.recordFieldCount];

	// read all record fields into the byte array.
	this.randomAccessFile.read(recordBytes);

	// convert the byte array into a String.
	String recordString = new String(recordBytes, US_ASCII);

	// declare and initialize the current field index/pointer.
	int startIndex = 0;

	// extract the fields from the record string.
	for (int index = 0; index < this.recordFieldCount; ++index) {

	    // substring the field represented by the for index into the string array.
	    recordFields[index] = recordString.substring(startIndex, startIndex + this.fieldLengths[index]);

	    // move the index to the next field.
	    startIndex += this.fieldLengths[index];
	}

	Log.logInfo("FileAccess.readRecordFromFile() successfully read record " + Long.toString(recNo));

	return recordFields;
    }

    /**
     * A class that represents a record cache. The parent {@link FileAccess} class uses one instance of this class as a
     * member variable. This class is initialized once when the {@link FileAccess} is created. This class is an
     * identical copy of all records in the database file. All changes done to the database file (like
     * add/delete/update) are immediately made in this class too. The database records and the records in this class
     * must always be identical.
     * 
     * <p>
     * The main purpose of this class it to provide fast read and search access to the data without accessing the
     * database file.
     * 
     * <p>
     * The class uses internally a <code>HashMap&lt;Long, Record&gt;</code> key/value map where the record number
     * represents the key and the {@link Record} class represents the record's flag and fields.
     * 
     * <p>
     * Note the parameters of all functions in this class are not validated, this is the responsibility of the caller
     * function(s).
     */
    private class RecordsCache {

	/**
	 * Key/value map of all records in the database file.
	 */
	Map<Long, Record> recordsMap = new HashMap<Long, Record>();

	/**
	 * Returns the record as a string array from the internal records map.
	 * 
	 * @param recordNumber
	 *            The number of the record to be returned.
	 * @return The record fields as a string array where each value represents a record field.
	 */
	String[] readRecord(long recordNumber) {

	    return this.recordsMap.get(recordNumber).getData();
	}

	/**
	 * Adds record to the records map.
	 * 
	 * @param recordNumber
	 *            The number of the record to be added. This value represents the records map key.
	 * @param isDeleted
	 *            True if the record is marked as deleted, false otherwise.
	 * @param data
	 *            The record fields as a string array, each value in the array represents one record field.
	 */
	void addRecord(long recordNumber, boolean isDeleted, String[] data) {

	    this.recordsMap.put(recordNumber, new Record(isDeleted, data));
	}

	/**
	 * Updates the field data of the record referred to by <code>recordNumber</code>
	 * 
	 * @param recordNumber
	 *            The record number to be updated.
	 * @param data
	 *            The record fields as a string array, each value in the array represents one record field.
	 */
	void writeRecord(long recordNumber, String[] data) {

	    this.recordsMap.get(recordNumber).setData(data);
	}

	/**
	 * Marks the record specified by <code>recordNumber</code> as deleted. Note the record is not removed from the
	 * cache. This function simply calls the {@link Record#deleteRecord()} of the corresponding {@link Record}
	 * class.
	 * 
	 * @param recordNumber
	 *            The number of the record to be marked as deleted.
	 */
	void deleteRecord(long recordNumber) {
	    this.recordsMap.get(recordNumber).deleteRecord();
	}

	/**
	 * Marks the record specified by <code>recordNumber</code> as undeleted. Calls the
	 * {@link Record#undeleteRecord()} of the corresponding {@link Record} class.
	 * 
	 * @param recordNumber
	 *            The number of the record to be marked as undeleted.
	 */
	void undeleteRecord(long recordNumber) {
	    this.recordsMap.get(recordNumber).undeleteRecord();
	}

	/**
	 * Determines if the record specified by <code>recordNumber</code> is deleted.
	 * 
	 * @param recordNumber
	 *            The number of the record.
	 * @return True if the records flag is marked as deleted, returns false otherwise.
	 */
	boolean isRecordDeleted(long recordNumber) {

	    return this.recordsMap.get(recordNumber).isRecordDeleted();
	}

	/**
	 * Determines if the record name and location fields are unique. This function is called in two situations:
	 * <ol>
	 * <li>When the caller wants to add a new record to the database. In this case the name and location parameters
	 * are compared to the name and location fields of all undeleted records in the cache.</li>
	 * <li>When the caller wants to update an existing record. In this case the new name and location parameters are
	 * compared to the name and location fields of all records in the cache except the record referred to by
	 * <code>skipRecordNumber</code> which should represent the record number of the record to be updated.</li>
	 * </ol>
	 * 
	 * <p>
	 * The function does not validate it's parameters, this is done by the callers. The case of the name and
	 * location values are are ignored.
	 * 
	 * @param newName
	 *            The name field value of the record.
	 * @param newLocation
	 *            The location field value of the record.
	 * @param skipRecordNumber
	 *            Specifies the record number to be skipped in the search. Provide -1 if you want to check the
	 *            uniqueness of the name and location parameters against all records in the cache. Provide a valid
	 *            record number that refers to the record you want to exclude from the search, this is usually the
	 *            number of the record you are updating.
	 * @return True if the name and location parameters are unique, false otherwise.
	 */
	boolean isNameLocationUnique(String newName, String newLocation, long skipRecordNumber) {

	    String currentName;
	    String currentLocation;

	    String newNameTrimmed = newName.trim();
	    String newLocationTrimmed = newLocation.trim();

	    // iterate all records in the cache.
	    for (long i = 0; i < this.recordsMap.size(); ++i) {

		// exclude deleted records.
		if (this.recordsMap.get(i).isRecordDeleted)
		    continue;

		// exclude the update record.
		if (i == skipRecordNumber) {
		    continue;
		}

		// retrieve the name and location fields of the i record and trim them.
		currentName = this.recordsMap.get(i).getData()[Data.FIELD_INDEX_NAME].trim();
		currentLocation = this.recordsMap.get(i).getData()[Data.FIELD_INDEX_LOCATION].trim();

		if (newNameTrimmed.equalsIgnoreCase(currentName)
			&& newLocationTrimmed.equalsIgnoreCase(currentLocation)) {

		    Log.logFine("FileAccess.isNameLocationUnique() returns false for: " + newName + ", " + newLocation);

		    return false;
		}
	    }

	    Log.logFine("FileAccess.isNameLocationUnique() returns true for: " + newName + ", " + newLocation);

	    return true;
	}

	/**
	 * Determines if the specified record contains an empty owner field or not.
	 * 
	 * @param recNo
	 *            The record number whose owner field is to be checked.
	 * @return Returns the value of: {@link Record#isRecordBooked()}
	 */
	boolean isRecordBooked(long recNo) {

	    if (!this.recordsMap.containsKey(recNo)) {
		throw new IllegalStateException("Record does not exist in the cache!");
	    }

	    return this.recordsMap.get(recNo).isRecordBooked();
	}

	/**
	 * Finds the first deleted record in the cache. First means the deleted record with the smallest record number.
	 * 
	 * @return The record number of the first deleted record or -1 if no deleted record is found.
	 */
	long getFirstDeletedRecord() {

	    for (long i = 0; i < this.recordsMap.size(); ++i) {

		if (this.recordsMap.get(i).isRecordDeleted) {

		    return i;
		}
	    }

	    return -1;
	}

	/**
	 * Represents a cached database record. The class consists of a boolean member variable
	 * <code>isRecordDeleted</code> and <code>data</code> string array variable.
	 * 
	 */
	private class Record {

	    /**
	     * true if the record is marked as deleted, false otherwise.
	     */
	    boolean isRecordDeleted = false;

	    /**
	     * The record's field data. Each value in the array corresponds to one field.
	     */
	    String[] data;

	    /**
	     * Constructs the class using the record <code>isDeleted</code> marker and the record's fields data.
	     * 
	     * @param isDeleted
	     *            true if the record is marked as deleted, false otherwise.
	     * 
	     * @param data
	     *            The record's fields data.
	     */
	    Record(boolean isDeleted, String[] data) {

		this.isRecordDeleted = isDeleted;
		this.data = data;
	    }

	    /**
	     * Returns the record's data as string array.
	     * 
	     * @return The record's data as string array.
	     */
	    String[] getData() {
		return this.data;
	    }

	    /**
	     * Sets the class internal <code>data</code> member variable to the value specified by the input parameter.
	     * 
	     * @param data
	     *            The record's fields as a string array.
	     */
	    void setData(String[] data) {
		this.data = data;
	    }

	    /**
	     * Returns true if the record is deleted, returns false otherwise.
	     * 
	     * @return True if the record is deleted, returns false otherwise.
	     */
	    boolean isRecordDeleted() {
		return this.isRecordDeleted;
	    }

	    /**
	     * Determines if the record is booked to an owner or not. A record is booked if the owner field contains a
	     * valid customer ID.
	     * 
	     * @return True if the owner field is not null and not empty string, false other wize.
	     */
	    boolean isRecordBooked() {

		if (this.data[Data.FIELD_INDEX_OWNER] == null) {
		    return false;
		}

		return !this.data[Data.FIELD_INDEX_OWNER].trim().isEmpty();
	    }

	    /**
	     * Marks the record as deleted.
	     */
	    void deleteRecord() {

		this.isRecordDeleted = true;
	    }

	    /**
	     * Marks the record as undeleted.
	     */
	    void undeleteRecord() {
		this.isRecordDeleted = false;
	    }
	}
    }

}
